from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from pathlib import Path
from django.conf import settings
import os
# import json

# 获取项目根目录路径
PROJECT_ROOT = settings.BASE_DIR  # 通常指向包含manage.py的目录

# 知识库配置
KNOWLEDGE_DIR = Path(PROJECT_ROOT) / "knowledge_base"  # 使用Path对象
PERSIST_DIR = Path(PROJECT_ROOT) / "vector_db"  # 使用Path对象
EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"

# 设置镜像环境变量
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"


class KnowledgeManager:

    def __init__(self):
        # 确保目录存在
        os.makedirs(PERSIST_DIR, exist_ok=True)
        self.embedding = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
        self.text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=300,  # 分块大小
            chunk_overlap=50,  # 重叠量
            separators=["\n\n", "\n", "。", "；", "====", "\n=======\n"])

        # 初始化向量数据库
        if not any(PERSIST_DIR.iterdir()):  # 如果目录为空
            self._init_knowledge_base()
        else:
            try:
                self.vector_db = Chroma(persist_directory=str(PERSIST_DIR),
                                        embedding_function=self.embedding)
                # 验证是否有效
                _ = self.vector_db.similarity_search("test", k=1)
            except Exception:
                self._init_knowledge_base()

    def _init_knowledge_base(self):
        """加载初始知识库"""
        # 清空旧数据库
        if PERSIST_DIR.exists():
            import shutil
            shutil.rmtree(PERSIST_DIR)

        docs = []
        for filename in KNOWLEDGE_DIR.glob("*.txt"):  # 使用Path对象遍历
            print(f"[DEBUG] 加载文件: {filename}")  # 调试日志
            with open(filename, 'r', encoding='utf-8') as f:
                text = f.read()
                splits = self.text_splitter.create_documents(
                    [text],
                    metadatas=[{
                        "source": filename.name
                    }]  # 添加元数据
                )
                print(f"[DEBUG] 分割为 {len(splits)} 个块，" +
                      f"示例内容: {splits[0].page_content}")  # 打印分割内容
                docs.extend(splits)

        self.vector_db = Chroma.from_documents(
            documents=docs,
            embedding=self.embedding,
            persist_directory=str(PERSIST_DIR)  # 路径转换
        )
        self.vector_db.persist()

        print("\n=== 知识库分块验证 ===")
        for i, doc in enumerate(docs):
            print(f"[块{i+1}] {doc.page_content}\n{'-'*50}")

    def retrieve_knowledge(self, query, k=5):
        """语义检索知识条目"""
        print(f"[DEBUG] 执行检索，查询: {query}")  # 调试日志
        results = self.vector_db.similarity_search(query, k=k)
        print(f"[DEBUG] 检索到 {len(results)} 条结果")  # 调试日志
        return results

    def purge_vector_db(self):
        """清空向量数据库"""
        self.vector_db.delete_collection()
        self.vector_db.persist()
        print("向量数据库已重置")


def parse_locations(text, history=[]):
    # 初始化知识管理器
    km = KnowledgeManager()

    # 语义检索
    relevant_docs = km.retrieve_knowledge(text)
    knowledge_context = "\n".join([doc.page_content for doc in relevant_docs])

    # 初始化llm
    deepseek = ChatOpenAI(
        # 模型deepseek-R1的key
        # openai_api_key='sk-4dfb5535ec564e6ba7d0dcfa83f342c7',
        # 模型deepseek-v3的key
        openai_api_key='sk-50b5c2c9c7534ac88337a7117e826658',
        base_url='https://api.deepseek.com/v1',  # 有没有最后的v1无所谓的，都支持；
        model='deepseek-chat',  # 模型deepseek-v3
        # model='deepseek-reasoner',  # 模型deepseek-R1
        temperature=0,  # temperature控制回答随机性（0-1）(0更稳定，1更激进)
        # 注意：deepseek-R1不支持结构化输出。所以下面的参数只能给deepseek-v3用
        model_kwargs={  # 新增模型参数
            'response_format': {
                'type': 'json_object'  # 指定输出模式，使用deepseek-v3的JSON OUTPUT模式输出
            }
        })

    # 构建上下文提示
    history_context = "\n".join([
        f"{'用户' if msg['role']=='user' else '系统'}: {msg['content']}"
        for msg in history[:]
    ])  # 添加历史聊天记录

    template = """你是一个基于大语言模型的旅游地图情景化生成系统，需要结合对话历史、相关知识库按照系统要求进行响应。

    ===对话历史===
    {history}

    ===相关知识库===
    {knowledge}

    ===当前输入===
    {text}

    ===系统要求===
    -- 需要结合历史对话中的地点信息进行智能补全。
    -- 当历史对话中已明确出发地/目的地时，优先使用历史信息。
    -- 整个系统只支持中国境内的地点。
    -- 你需要理解用户输入，然后根据输出格式中各参数的描述合理设定参数，最后输出。
    -- 对于styleJson部分，你需要根据实际情况决定是否使用所有的参数和省略不必要的参数。但除了styleJson以外的部分你不能省略参数。
    -- 你需要严格按照输出格式输出json格式的结果。
    -- 不要使用markdown格式进行输出，只输出纯文本。
    -- 注意在所有}}前的最后一组键值对的结尾不要有逗号。
    -- 注意JSON格式的合法性（正确使用引号/逗号/括号）
    -- 当知识库包含状态变更（如关闭通知）时，必须优先遵守
    -- 如果运用了相关知识库的内容，需在advice字段体现应用
    -- 如果根据相关知识库，你无法向用户提供用户想去的目标路线，则需要为用户选择替代品，并在advice字段详细说明原因。
    -- 推荐替代方案时需参考知识库中的关联地点
    -- 根据相关知识库得到的用户对于地图样式的倾向信息，需要根据实际情况判断是否有必要修改styleJson字段

    ===输出格式及参数描述===
    {{
        "getRoutePath": <是否需要进行导航。你需要判断用户是否想去旅游，来找你要旅游路线。
                        如果是的话，这个值设定为true，反之设定为false。
                        这个值和searchPlace不能同时设定为true。>,
        "getRoutePathParameter": {{
            "startLocation": <导航的出发地点。
                            如果用户的输入中有明确的出发地点，这里就用字符串写清出发点；
                            如果没有明确的出发点，则设定为一个空的字符串。>,
            "endLocation": <导航的目的地。
                            如果用户的输入中有明确的导航目的地，这里就用字符串写清目的地；
                            如果没有明确的目的地，则需要你根据用户的意图为用户提供一个最佳目的地（仅限中国境内），然后用字符串写清目的地；
                            这里不能是空的字符串，且一定是一个详细的地址名称。>,
            "startCity": <出发地点的所属城市。
                            如果用户有明确的出发地点城市，则用字符串写清；
                            如果没有明确的出发地点城市，则尝试根据前面的startLocation参数判断出发地点的城市；
                            如果无法判断出发地点的城市，则这里设定为一个空的字符串。>,
            "endCity": <导航目的地的所属城市。
                            如果用户有明确的导航目的地所属城市，则用字符串写清；
                            如果没有明确的导航目的地所属城市，则尝试根据前面的endLocation参数判断导航目的地所属城市；
                            如果无法判断导航目的地所属城市，则这里设定为一个空的字符串。>,
            "locate": <是否需要定位。
                            如果用户输入中缺乏明确的出发地点或者出发城市或者目的地所属城市，则设定为true；否则设定为false。>
        }},
        "searchPlace": <是否进行搜索。
                        如果用户的意图更偏向于进行搜索关键字而不是去一个具体的地方，如“我想看看成都有哪些公园”，则这个值设定为true；
                        否则设定为false；
                        这个值和getRoutePath不能同时设定为true。>,
        "searchPlaceParameter": {{
            "searchName": <用户想要搜索的地名。
                            用字符串写清搜索关键字。
                            在searchPlace为true时这个值不能为空字符串。>,
            "searchCity": <搜索的目标城市。
                            根据用户输入，推测用户想在哪一座城市进行搜索，用字符串写清。
                            这个值不能为空字符串。>
            }},
        "advice": <回应用户的纯文本内容。
                如果你将getRoutePath的值设定为true，则根据用户的旅游目标，想一条有价值，丰富的建议，默认用户是开车前往目的地，不要对具体的导航路线做任何建议；
                如果你将searchPlace的值设定为true，则根据用户的搜索目标，推荐可能用户更需要的具体名称。
                如果用户输入完全跟系统的功能无关，则生成适当的回复；>,
        "styleJson": <设定地图样式的参数。
                    需要你根据实际情况，仅添加用户想要更改的参数部分，其他部分不必添加>
        [
            {{
                // 当用户想要更改地图陆地部分样式时添加这部分参数
                "featureType": "land",          // 固定为字符串land
                "elementType": "geometry",      // 固定为字符串geometry
                "stylers": {{
                    "color": <使用ARGB十六进制颜色值表示颜色，用字符串写清，字符串最后额外加上FF
                            如"#0000FFFF"表示蓝色，"FF0000FF"表示红色>
                }}
            }},
            {{
                // 当用户想要更改地图水域部分样式时添加这部分参数
                "featureType": "water",         // 固定为字符串water
                "elementType": "geometry",      // 固定为字符串geometry
                "stylers": {{
                    "color": <使用ARGB十六进制颜色值表示颜色，用字符串写清，字符串最后额外加上FF
                            如"#0000FFFF"表示蓝色，"FF0000FF"表示红色>
                    }}
            }},
            {{
                // 当用户想要更改地图绿地、植被部分样式时添加这部分参数
                "featureType": "green",         // 固定为字符串green
                "elementType": "geometry",      // 固定为字符串geometry
                "stylers": {{
                    "color": <使用ARGB十六进制颜色值表示颜色，用字符串写清，字符串最后额外加上FF
                            如"#0000FFFF"表示蓝色，"FF0000FF"表示红色>
                    }}
            }},
            {{
                // 当用户想要更改高速公路填充内容部分样式时添加这部分参数
                "featureType": "highway",      // 固定为字符串highway
                "elementType": "geometry.fill",// 固定为字符串geometry.fill
                "stylers": {{
                    "color": <使用ARGB十六进制颜色值表示颜色，用字符串写清，字符串最后额外加上FF
                            如"#0000FFFF"表示蓝色，"FF0000FF"表示红色>
                    }}
            }}
        ]
    }}

    ===输入输出示例===
    示例输入1：
    知识条目：无
    用户输入：我想去杭州西湖玩
    示例JSON输出：
    {{
        "getRoutePath": true,
        "getRoutePathParameter": {{
            "startLocation": "",
            "endLocation": "西湖景区",
            "startCity": "",
            "endCity": "杭州市",
            "locate": true
        }},
        "searchPlace": false,
        "searchPlaceParameter": {{
            "searchName": "",
            "searchCity": ""
        }},
        "advice": "西湖景区周边停车位紧张，建议使用公共交通前往。",
        "styleJson": []
    }}

    示例输入2：
    知识条目：成都九峰山无限期封山
    用户输入：我想去爬成都的九峰山
    正确响应：{{
        "getRoutePath": true,
        "getRoutePathParameter": {{
            "startLocation": "",
            "endLocation": "赵公山",
            "startCity": "",
            "endCity": "成都市",
            "locate": true
        }},
        "searchPlace": false,
        "searchPlaceParameter": {{
            "searchName": "",
            "searchCity": ""
        }},
        "advice": "九峰山已经无限期封山，作为替代，向您推荐同样是成都周边的赵公山。赵公山和九峰山一样，都是...",
        "styleJson": []
        }}

    示例输入3：
    知识条目：对于喜欢爬山的用户，他们会更喜欢把地图上的高速路换成蓝色
    用户输入：我想去爬成都的赵公山
    正确响应：{{
        "getRoutePath": true,
        "getRoutePathParameter": {{
            "startLocation": "",
            "endLocation": "赵公山",
            "startCity": "",
            "endCity": "成都市",
            "locate": true
        }},
        "searchPlace": false,
        "searchPlaceParameter": {{
            "searchName": "",
            "searchCity": ""
        }},
        "advice": "赵公山是成都市周边的一座山峰，商业化程度低，具有“野趣”，适合进行徒步活动。同时，为您将地图上的高速路换成了蓝色。",
        "styleJson": [
            {{
            "featureType": "highway",
            "elementType": "geometry.fill",
            "stylers": {{
                "color": "#0000FFFF"
            }}
        }}
        ]
        }}
    """
    prompt = PromptTemplate(template=template,
                            input_variables=["text", "history", "knowledge"])
    chain = LLMChain(llm=deepseek, prompt=prompt)

    # 在chain.run前添加调试输出
    print("\n=== 检索到的知识条目 ===")
    if not relevant_docs:
        print("⚠️ 未检索到任何相关知识条目")
    else:
        for i, doc in enumerate(relevant_docs):
            print(f"[知识块{i+1}] {doc.page_content}\n")

    output = chain.run({
        "text": text,
        "history": history_context,
        "knowledge": knowledge_context
    })

    # 古法调试：在终端显示output，出bug了能查
    print()
    print(output)
    print()

    return output


# 初始化知识库目录结构
os.makedirs(KNOWLEDGE_DIR, exist_ok=True)
os.makedirs(PERSIST_DIR, exist_ok=True)
